001 /* 002 * Copyright 2003-2005 The Apache Software Foundation 003 * Copyright 2005 Stephen McConnell 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package net.dpml.cli.option; 018 019 import java.util.ArrayList; 020 import java.util.Collection; 021 import java.util.Collections; 022 import java.util.Comparator; 023 import java.util.HashSet; 024 import java.util.Iterator; 025 import java.util.List; 026 import java.util.ListIterator; 027 import java.util.Map; 028 import java.util.Set; 029 import java.util.SortedMap; 030 import java.util.TreeMap; 031 032 import net.dpml.cli.Argument; 033 import net.dpml.cli.DisplaySetting; 034 import net.dpml.cli.Group; 035 import net.dpml.cli.HelpLine; 036 import net.dpml.cli.Option; 037 import net.dpml.cli.OptionException; 038 import net.dpml.cli.WriteableCommandLine; 039 import net.dpml.cli.resource.ResourceConstants; 040 041 /** 042 * An implementation of Group 043 * @author <a href="http://www.dpml.net">Digital Product Meta Library</a> 044 * @version 1.0.0 045 */ 046 public class GroupImpl extends OptionImpl implements Group 047 { 048 private final String m_name; 049 private final String m_description; 050 private final List m_options; 051 private final int m_minimum; 052 private final int m_maximum; 053 private final List m_anonymous; 054 private final SortedMap m_optionMap; 055 private final Set m_prefixes; 056 057 /** 058 * Creates a new GroupImpl using the specified parameters. 059 * 060 * @param options the Options and Arguments that make up the Group 061 * @param name the name of this Group, or null 062 * @param description a description of this Group 063 * @param minimum the minimum number of Options for a valid CommandLine 064 * @param maximum the maximum number of Options for a valid CommandLine 065 */ 066 public GroupImpl( 067 final List options, final String name, final String description, 068 final int minimum, final int maximum ) 069 { 070 super( 0, false ); 071 072 m_name = name; 073 m_description = description; 074 m_minimum = minimum; 075 m_maximum = maximum; 076 077 // store a copy of the options to be used by the 078 // help methods 079 m_options = Collections.unmodifiableList( options ); 080 081 // m_anonymous Argument temporary storage 082 final List newAnonymous = new ArrayList(); 083 084 // map (key=trigger & value=Option) temporary storage 085 final SortedMap newOptionMap = new TreeMap( ReverseStringComparator.getInstance() ); 086 087 // prefixes temporary storage 088 final Set newPrefixes = new HashSet(); 089 090 // process the options 091 for( final Iterator i = options.iterator(); i.hasNext();) 092 { 093 final Option option = (Option) i.next(); 094 if( option instanceof Argument ) 095 { 096 i.remove(); 097 newAnonymous.add( option ); 098 } 099 else 100 { 101 final Set triggers = option.getTriggers(); 102 for( Iterator j = triggers.iterator(); j.hasNext();) 103 { 104 newOptionMap.put( j.next(), option ); 105 } 106 // store the prefixes 107 newPrefixes.addAll( option.getPrefixes() ); 108 } 109 } 110 111 m_anonymous = Collections.unmodifiableList( newAnonymous ); 112 m_optionMap = Collections.unmodifiableSortedMap( newOptionMap ); 113 m_prefixes = Collections.unmodifiableSet( newPrefixes ); 114 } 115 116 /** 117 * Indicates whether this Option will be able to process the particular 118 * argument. 119 * 120 * @param commandLine the CommandLine object to store defaults in 121 * @param arg the argument to be tested 122 * @return true if the argument can be processed by this Option 123 */ 124 public boolean canProcess( 125 final WriteableCommandLine commandLine, final String arg ) 126 { 127 if( arg == null ) 128 { 129 return false; 130 } 131 132 // if arg does not require bursting 133 if( m_optionMap.containsKey( arg ) ) 134 { 135 return true; 136 } 137 138 // filter 139 final Map tailMap = m_optionMap.tailMap( arg ); 140 141 // check if bursting is required 142 for( final Iterator iter = tailMap.values().iterator(); iter.hasNext();) 143 { 144 final Option option = (Option) iter.next(); 145 if( option.canProcess( commandLine, arg ) ) 146 { 147 return true; 148 } 149 } 150 151 if( commandLine.looksLikeOption( arg ) ) 152 { 153 return false; 154 } 155 156 // m_anonymous argument(s) means we can process it 157 if( m_anonymous.size() > 0 ) 158 { 159 return true; 160 } 161 162 return false; 163 } 164 165 /** 166 * Identifies the argument prefixes that should be considered options. This 167 * is used to identify whether a given string looks like an option or an 168 * argument value. Typically an option would return the set [--,-] while 169 * switches might offer [-,+]. 170 * 171 * The returned Set must not be null. 172 * 173 * @return The set of prefixes for this Option 174 */ 175 public Set getPrefixes() 176 { 177 return m_prefixes; 178 } 179 180 /** 181 * Identifies the argument prefixes that should trigger this option. This 182 * is used to decide which of many Options should be tried when processing 183 * a given argument string. 184 * 185 * The returned Set must not be null. 186 * 187 * @return The set of triggers for this Option 188 */ 189 public Set getTriggers() 190 { 191 return m_optionMap.keySet(); 192 } 193 194 /** 195 * Processes String arguments into a CommandLine. 196 * 197 * The iterator will initially point at the first argument to be processed 198 * and at the end of the method should point to the first argument not 199 * processed. This method MUST process at least one argument from the 200 * ListIterator. 201 * 202 * @param commandLine the CommandLine object to store results in 203 * @param arguments the arguments to process 204 * @throws OptionException if any problems occur 205 */ 206 public void process( 207 final WriteableCommandLine commandLine, final ListIterator arguments ) 208 throws OptionException 209 { 210 String previous = null; 211 212 // [START process each command line token 213 while( arguments.hasNext() ) 214 { 215 // grab the next argument 216 final String arg = (String) arguments.next(); 217 218 // if we have just tried to process this instance 219 if( arg == previous ) 220 { 221 // rollback and abort 222 arguments.previous(); 223 break; 224 } 225 226 // remember last processed instance 227 previous = arg; 228 229 final Option opt = (Option) m_optionMap.get( arg ); 230 231 // option found 232 if( opt != null ) 233 { 234 arguments.previous(); 235 opt.process( commandLine, arguments ); 236 } 237 // [START option NOT found 238 else 239 { 240 // it might be an m_anonymous argument continue search 241 // [START argument may be m_anonymous 242 if( commandLine.looksLikeOption( arg ) ) 243 { 244 // narrow the search 245 final Collection values = m_optionMap.tailMap( arg ).values(); 246 boolean foundMemberOption = false; 247 for( Iterator i = values.iterator(); i.hasNext() && !foundMemberOption;) 248 { 249 final Option option = (Option) i.next(); 250 if( option.canProcess( commandLine, arg ) ) 251 { 252 foundMemberOption = true; 253 arguments.previous(); 254 option.process( commandLine, arguments ); 255 } 256 } 257 258 // back track and abort this group if necessary 259 if( !foundMemberOption ) 260 { 261 arguments.previous(); 262 return; 263 } 264 265 } // [END argument may be m_anonymous 266 // [START argument is NOT m_anonymous 267 else 268 { 269 // move iterator back, current value not used 270 arguments.previous(); 271 272 // if there are no m_anonymous arguments then this group can't 273 // process the argument 274 if( m_anonymous.isEmpty() ) 275 { 276 break; 277 } 278 279 // why do we iterate over all m_anonymous arguments? 280 // canProcess will always return true? 281 for( final Iterator i = m_anonymous.iterator(); i.hasNext();) 282 { 283 final Argument argument = (Argument) i.next(); 284 if( argument.canProcess( commandLine, arguments ) ) 285 { 286 argument.process( commandLine, arguments ); 287 } 288 } 289 } // [END argument is NOT m_anonymous 290 } // [END option NOT found 291 } // [END process each command line token 292 } 293 294 /** 295 * Checks that the supplied CommandLine is valid with respect to this 296 * option. 297 * 298 * @param commandLine the CommandLine to check. 299 * @throws OptionException if the CommandLine is not valid. 300 */ 301 public void validate( final WriteableCommandLine commandLine ) throws OptionException 302 { 303 // number of options found 304 int present = 0; 305 306 // reference to first unexpected option 307 Option unexpected = null; 308 309 for( final Iterator i = m_options.iterator(); i.hasNext();) 310 { 311 final Option option = (Option) i.next(); 312 313 // if the child option is required then validate it 314 if( option.isRequired() ) 315 { 316 option.validate( commandLine ); 317 } 318 319 if( option instanceof Group ) 320 { 321 option.validate( commandLine ); 322 } 323 324 // if the child option is present then validate it 325 if( commandLine.hasOption( option ) ) 326 { 327 if( ++present > m_maximum ) 328 { 329 unexpected = option; 330 break; 331 } 332 option.validate( commandLine ); 333 } 334 } 335 336 // too many options 337 if( unexpected != null ) 338 { 339 throw new OptionException( 340 this, 341 ResourceConstants.UNEXPECTED_TOKEN, 342 unexpected.getPreferredName() ); 343 } 344 345 // too few option 346 if( present < m_minimum ) 347 { 348 throw new OptionException( 349 this, 350 ResourceConstants.MISSING_OPTION ); 351 } 352 353 // validate each m_anonymous argument 354 for( final Iterator i = m_anonymous.iterator(); i.hasNext();) 355 { 356 final Option option = (Option) i.next(); 357 option.validate( commandLine ); 358 } 359 } 360 361 /** 362 * The preferred name of an option is used for generating help and usage 363 * information. 364 * 365 * @return The preferred name of the option 366 */ 367 public String getPreferredName() 368 { 369 return m_name; 370 } 371 372 /** 373 * Returns a description of the option. This string is used to build help 374 * messages as in the HelpFormatter. 375 * 376 * @see net.dpml.cli.util.HelpFormatter 377 * @return a description of the option. 378 */ 379 public String getDescription() 380 { 381 return m_description; 382 } 383 384 /** 385 * Appends usage information to the specified StringBuffer 386 * 387 * @param buffer the buffer to append to 388 * @param helpSettings a set of display settings @see DisplaySetting 389 * @param comp a comparator used to sort the Options 390 */ 391 public void appendUsage( 392 final StringBuffer buffer, final Set helpSettings, final Comparator comp ) 393 { 394 if( getMaximum() == 1 ) 395 { 396 appendUsage( buffer, helpSettings, comp, "|" ); 397 } 398 else 399 { 400 appendUsage( buffer, helpSettings, comp, " " ); 401 } 402 } 403 404 /** 405 * Appends usage information to the specified StringBuffer 406 * 407 * @param buffer the buffer to append to 408 * @param helpSettings a set of display settings @see DisplaySetting 409 * @param comp a comparator used to sort the Options 410 * @param separator the String used to separate member Options 411 */ 412 public void appendUsage( 413 final StringBuffer buffer, final Set helpSettings, final Comparator comp, 414 final String separator ) 415 { 416 final Set helpSettingsCopy = new HashSet( helpSettings ); 417 418 final boolean optional = 419 ( m_minimum == 0 ) 420 && helpSettingsCopy.contains( DisplaySetting.DISPLAY_OPTIONAL ); 421 422 final boolean expanded = 423 ( m_name == null ) 424 || helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_EXPANDED ); 425 426 final boolean named = 427 !expanded 428 || ( ( m_name != null ) && helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_NAME ) ); 429 430 final boolean arguments = 431 helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_ARGUMENT ); 432 433 final boolean outer = 434 helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_OUTER ); 435 436 helpSettingsCopy.remove( DisplaySetting.DISPLAY_GROUP_OUTER ); 437 438 final boolean both = named && expanded; 439 440 if( optional ) 441 { 442 buffer.append( '[' ); 443 } 444 445 if( named ) 446 { 447 buffer.append( m_name ); 448 } 449 450 if( both ) 451 { 452 buffer.append( " (" ); 453 } 454 455 if( expanded ) 456 { 457 final Set childSettings; 458 459 if( !helpSettingsCopy.contains( DisplaySetting.DISPLAY_GROUP_EXPANDED ) ) 460 { 461 childSettings = DisplaySetting.NONE; 462 } 463 else 464 { 465 childSettings = new HashSet( helpSettingsCopy ); 466 childSettings.remove( DisplaySetting.DISPLAY_OPTIONAL ); 467 } 468 469 // grab a list of the group's options. 470 final List list; 471 472 if( comp == null ) 473 { 474 // default to using the initial order 475 list = m_options; 476 } 477 else 478 { 479 // sort options if comparator is supplied 480 list = new ArrayList( m_options ); 481 Collections.sort( list, comp ); 482 } 483 484 // for each option. 485 for( final Iterator i = list.iterator(); i.hasNext();) 486 { 487 final Option option = (Option) i.next(); 488 489 // append usage information 490 option.appendUsage( buffer, childSettings, comp ); 491 492 // add separators as needed 493 if( i.hasNext() ) 494 { 495 buffer.append( separator ); 496 } 497 } 498 } 499 500 if( both ) 501 { 502 buffer.append( ')' ); 503 } 504 505 if( optional && outer ) 506 { 507 buffer.append( ']' ); 508 } 509 510 if( arguments ) 511 { 512 for( final Iterator i = m_anonymous.iterator(); i.hasNext();) 513 { 514 buffer.append( ' ' ); 515 final Option option = (Option) i.next(); 516 option.appendUsage( buffer, helpSettingsCopy, comp ); 517 } 518 } 519 520 if( optional && !outer ) 521 { 522 buffer.append( ']' ); 523 } 524 } 525 526 /** 527 * Builds up a list of HelpLineImpl instances to be presented by HelpFormatter. 528 * 529 * @see HelpLine 530 * @see net.dpml.cli.util.HelpFormatter 531 * @param depth the initial indent depth 532 * @param helpSettings the HelpSettings that should be applied 533 * @param comp a comparator used to sort options when applicable. 534 * @return a List of HelpLineImpl objects 535 */ 536 public List helpLines( 537 final int depth, final Set helpSettings, final Comparator comp ) 538 { 539 final List helpLines = new ArrayList(); 540 541 if( helpSettings.contains( DisplaySetting.DISPLAY_GROUP_NAME ) ) 542 { 543 final HelpLine helpLine = new HelpLineImpl( this, depth ); 544 helpLines.add( helpLine ); 545 } 546 547 if( helpSettings.contains( DisplaySetting.DISPLAY_GROUP_EXPANDED ) ) 548 { 549 // grab a list of the group's options. 550 final List list; 551 552 if( comp == null ) 553 { 554 // default to using the initial order 555 list = m_options; 556 } 557 else 558 { 559 // sort options if comparator is supplied 560 list = new ArrayList( m_options ); 561 Collections.sort( list, comp ); 562 } 563 564 // for each option 565 for( final Iterator i = list.iterator(); i.hasNext();) 566 { 567 final Option option = (Option) i.next(); 568 helpLines.addAll( option.helpLines( depth + 1, helpSettings, comp ) ); 569 } 570 } 571 572 if( helpSettings.contains( DisplaySetting.DISPLAY_GROUP_ARGUMENT ) ) 573 { 574 for( final Iterator i = m_anonymous.iterator(); i.hasNext();) 575 { 576 final Option option = (Option) i.next(); 577 helpLines.addAll( option.helpLines( depth + 1, helpSettings, comp ) ); 578 } 579 } 580 581 return helpLines; 582 } 583 584 /** 585 * Gets the member Options of thie Group. 586 * Note this does not include any Arguments 587 * @return only the non Argument Options of the Group 588 */ 589 public List getOptions() 590 { 591 return m_options; 592 } 593 594 /** 595 * Gets the m_anonymous Arguments of this Group. 596 * @return the Argument options of this Group 597 */ 598 public List getAnonymous() 599 { 600 return m_anonymous; 601 } 602 603 /** 604 * Recursively searches for an option with the supplied trigger. 605 * 606 * @param trigger the trigger to search for. 607 * @return the matching option or null. 608 */ 609 public Option findOption( final String trigger ) 610 { 611 final Iterator i = getOptions().iterator(); 612 613 while( i.hasNext() ) 614 { 615 final Option option = (Option) i.next(); 616 final Option found = option.findOption( trigger ); 617 if( found != null ) 618 { 619 return found; 620 } 621 } 622 return null; 623 } 624 625 /** 626 * Retrieves the minimum number of values required for a valid Argument 627 * 628 * @return the minimum number of values 629 */ 630 public int getMinimum() 631 { 632 return m_minimum; 633 } 634 635 /** 636 * Retrieves the maximum number of values acceptable for a valid Argument 637 * 638 * @return the maximum number of values 639 */ 640 public int getMaximum() 641 { 642 return m_maximum; 643 } 644 645 /** 646 * Indicates whether argument values must be present for the CommandLine to 647 * be valid. 648 * 649 * @see #getMinimum() 650 * @see #getMaximum() 651 * @return true iff the CommandLine will be invalid without at least one 652 * value 653 */ 654 public boolean isRequired() 655 { 656 return getMinimum() > 0; 657 } 658 659 /** 660 * Process defaults. 661 * @param commandLine the commandline 662 */ 663 public void defaults( final WriteableCommandLine commandLine ) 664 { 665 super.defaults( commandLine ); 666 for( final Iterator i = m_options.iterator(); i.hasNext();) 667 { 668 final Option option = (Option) i.next(); 669 option.defaults( commandLine ); 670 } 671 672 for( final Iterator i = m_anonymous.iterator(); i.hasNext();) 673 { 674 final Option option = (Option) i.next(); 675 option.defaults( commandLine ); 676 } 677 } 678 } 679 680 /** 681 * A reverse string comparator. 682 */ 683 final class ReverseStringComparator implements Comparator 684 { 685 private static final Comparator INSTANCE = new ReverseStringComparator(); 686 687 private ReverseStringComparator() 688 { 689 // static 690 } 691 692 /** 693 * Gets a singleton instance of a ReverseStringComparator 694 * @return the singleton instance 695 */ 696 public static final Comparator getInstance() 697 { 698 return INSTANCE; 699 } 700 701 /** 702 * Compare two instances. 703 * @param o1 the first instance 704 * @param o2 the second instance 705 * @return the result 706 */ 707 public int compare( final Object o1, final Object o2 ) 708 { 709 final String s1 = (String) o1; 710 final String s2 = (String) o2; 711 return -s1.compareTo( s2 ); 712 } 713 }